package com.wjf.shortlink.shortlink.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wjf.shortlink.shortlink.common.convention.exception.ClientException;
import com.wjf.shortlink.shortlink.common.convention.exception.ServiceException;
import com.wjf.shortlink.shortlink.common.enums.ValidDateTypeEnum;
import com.wjf.shortlink.shortlink.config.GotoDomainWhiteListConfiguration;
import com.wjf.shortlink.shortlink.dao.entity.ShortLinkDAO;
import com.wjf.shortlink.shortlink.dao.entity.ShortLinkGotoDAO;
import com.wjf.shortlink.shortlink.dao.mapper.ShortLinkGotoMapper;
import com.wjf.shortlink.shortlink.dao.mapper.ShortLinkMapper;
import com.wjf.shortlink.shortlink.dto.biz.ShortLinkStatsRecordDTO;
import com.wjf.shortlink.shortlink.dto.req.ShortLinkBatchCreateReqDTO;
import com.wjf.shortlink.shortlink.dto.req.ShortLinkCreateReqDTO;
import com.wjf.shortlink.shortlink.dto.req.ShortLinkPageReqDTO;
import com.wjf.shortlink.shortlink.dto.req.ShortLinkUpdateReqDTO;
import com.wjf.shortlink.shortlink.dto.resp.*;
import com.wjf.shortlink.shortlink.mq.producer.ShortLinkStatsSaveProducer;
import com.wjf.shortlink.shortlink.service.ShortLinkService;
import com.wjf.shortlink.shortlink.toolkit.HashUtil;
import com.wjf.shortlink.shortlink.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.wjf.shortlink.shortlink.common.constant.RedisKeyConstant.*;

//短链接接口实现层
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDAO> implements ShortLinkService {

    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;
    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private ShortLinkGotoMapper shortLinkGotoMapper;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    //创建短链接
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO reqDTO) {
        verificationWhitelist(reqDTO.getOriginUrl());
        String shortLinkSuffix = generateSuffix(reqDTO);
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();

        ShortLinkDAO shortLinkDAO = ShortLinkDAO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(reqDTO.getOriginUrl())
                .gid(reqDTO.getGid())
                .createdType(reqDTO.getCreatedType())
                .validDate(reqDTO.getValidDate())
                .validDateType(reqDTO.getCreatedType())
                .describe(reqDTO.getDescribe())
                .enableStatus(0)
                .shortUri(shortLinkSuffix)
                .todayPv(0)
                .todayUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(reqDTO.getOriginUrl()))
                .build();
        ShortLinkGotoDAO shortLinkGotoDAO = ShortLinkGotoDAO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(reqDTO.getGid())
                .build();
        try {
            baseMapper.insert(shortLinkDAO);
            shortLinkGotoMapper.insert(shortLinkGotoDAO);
        } catch (DuplicateKeyException e) {
            if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
                shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
            }
            // throw 保证事务回滚
            throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
        }
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                reqDTO.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(reqDTO.getValidDate()),
                TimeUnit.MICROSECONDS
        );
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDAO.getFullShortUrl())
                .originUrl(reqDTO.getOriginUrl())
                .gid(reqDTO.getGid())
                .build();
    }

    // 通过分布式锁创建短链接
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO reqDTO) {
        verificationWhitelist(reqDTO.getOriginUrl());
        String fullShortUrl;
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            String shortLinkSuffix = generateSuffix(reqDTO);
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();
            ShortLinkDAO shortLinkDAO = ShortLinkDAO.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(reqDTO.getOriginUrl())
                    .gid(reqDTO.getGid())
                    .createdType(reqDTO.getCreatedType())
                    .validDate(reqDTO.getValidDate())
                    .validDateType(reqDTO.getCreatedType())
                    .describe(reqDTO.getDescribe())
                    .shortUri(shortLinkSuffix)
                    .enableStatus(0)
                    .todayPv(0)
                    .todayUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .fullShortUrl(fullShortUrl)
                    .favicon(getFavicon(reqDTO.getOriginUrl()))
                    .build();
            ShortLinkGotoDAO shortLinkGotoDAO = ShortLinkGotoDAO.builder()
                    .gid(reqDTO.getGid())
                    .fullShortUrl(fullShortUrl)
                    .build();
            try {
                baseMapper.insert(shortLinkDAO);
                shortLinkGotoMapper.insert(shortLinkGotoDAO);
            } catch (DuplicateKeyException e) {
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    reqDTO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(reqDTO.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
        } finally {
            lock.unlock();
        }
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(reqDTO.getOriginUrl())
                .gid(reqDTO.getGid())
                .build();
    }

    //批量创建短链接
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO reqDTO) {
        List<String> originUrls = reqDTO.getOriginUrls();
        List<String> describes = reqDTO.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        Integer total = originUrls.size();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(reqDTO, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            try {
                // 每次循环都开启新的事务 ？？？？？
                //ShortLinkCreateRespDTO shortLinkCreateRespDTO = ((ShortLinkServiceImpl) AopContext.currentProxy()).createShortLink(shortLinkCreateReqDTO);
                ShortLinkCreateRespDTO shortLinkCreateRespDTO = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLinkCreateRespDTO.getFullShortUrl())
                        .originUrl(shortLinkCreateRespDTO.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable e) {
                // 保证事务回滚
                // throw new ClientException(String.format("批量创建短链接失败，原始参数：%s", originUrls.get(i)));
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
                total--;
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(total)
                .baseLinkInfos(result)
                .build();
    }

    //生成短链接后缀
    private String generateSuffix(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {
        int customGenerateCount = 0;
        String shortUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = shortLinkCreateReqDTO.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shortUri;
    }

    //校验短链接白名单
    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnabled();
        if (enable == null || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境未避免恶意攻击，请生成以下网址跳转链接："
                    + gotoDomainWhiteListConfiguration.getNames());
        }
    }

    //获取网址favicon
    @SneakyThrows
    private String getFavicon(String url) {
        URL target = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) target.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    //统计各分组中短链接数量
    public List<ShortLinkGroupCountQueryRespDTO> ListGroupShortLinkCount(List<String> gIds) {
        QueryWrapper<ShortLinkDAO> queryWrapper = Wrappers.query(new ShortLinkDAO())
                .select("gid as gid,count(*) as shortLinkCount")
                .in("gid", gIds)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDAOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDAOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + shortUri;
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalLink)) {
            shortLinkStats(buildShortLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originalLink)) {
                shortLinkStats(buildShortLinkStatsRecordAndSetUser(originalLink, request, response));
                ((HttpServletResponse) response).sendRedirect(originalLink);
                return;
            }
            gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<ShortLinkGotoDAO> linkGotoDAOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDAO.class)
                    .eq(ShortLinkGotoDAO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDAO shortLinkGotoDAO = shortLinkGotoMapper.selectOne(linkGotoDAOLambdaQueryWrapper);
            if (shortLinkGotoDAO == null) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<ShortLinkDAO> shortLinkDAOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkDAO.class)
                    .eq(ShortLinkDAO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDAO::getId, shortLinkGotoDAO.getId())
                    .eq(ShortLinkDAO::getDelFlag, 0)
                    .eq(ShortLinkDAO::getEnableStatus, 0);
            ShortLinkDAO shortLinkDAO = baseMapper.selectOne(shortLinkDAOLambdaQueryWrapper);
            if (shortLinkDAO == null || (shortLinkDAO.getValidDate() != null && shortLinkDAO.getValidDate().before(new Date()))) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDAO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDAO.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
            shortLinkStats(buildShortLinkStatsRecordAndSetUser(fullShortUrl, request, response));
            ((HttpServletResponse) response).sendRedirect(shortLinkDAO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    //统计短链接使用
    private ShortLinkStatsRecordDTO buildShortLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(true);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String remoteAddr = LinkUtil.getActualIp(httpServletRequest);
        String os = LinkUtil.getOs(httpServletRequest);
        String browser = LinkUtil.getBrowser(httpServletRequest);
        String device = LinkUtil.getDevice(httpServletRequest);
        String network = LinkUtil.getNetwork(httpServletRequest);
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO shortLinkStatsRecordDTO) {
        Map<String, String> produceMap = new HashMap<>();
        produceMap.put("statsRecord", JSON.toJSONString(shortLinkStatsRecordDTO));
        shortLinkStatsSaveProducer.send(produceMap);
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO reqDTO) {
        IPage<ShortLinkDAO> resultPage = baseMapper.pageLink(reqDTO);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }

    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO reqDTO) {
        verificationWhitelist(reqDTO.getOriginUrl());
        LambdaQueryWrapper<ShortLinkDAO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDAO.class)
                .eq(ShortLinkDAO::getGid, reqDTO.getOriginGid())
                .eq(ShortLinkDAO::getFullShortUrl, reqDTO.getFullShortUrl()) //不变
                .eq(ShortLinkDAO::getDelFlag, 0)
                .eq(ShortLinkDAO::getEnableStatus, 0);
        ShortLinkDAO shortLinkDAO = baseMapper.selectOne(queryWrapper);
        if (shortLinkDAO == null) {
            throw new ClientException("短链接记录不存在");
        }
        if (Objects.equals(shortLinkDAO.getGid(), reqDTO.getGid())) {
            LambdaUpdateWrapper<ShortLinkDAO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDAO.class)
                    .eq(ShortLinkDAO::getDelFlag, 0)
                    .eq(ShortLinkDAO::getEnableStatus, 0)
                    .eq(ShortLinkDAO::getFullShortUrl, reqDTO.getFullShortUrl())
                    .eq(ShortLinkDAO::getGid, reqDTO.getGid())
                    .set(Objects.equals(reqDTO.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()), ShortLinkDAO::getValidDate, null);
            ShortLinkDAO updateShortLinkDAO = ShortLinkDAO.builder()
                    .domain(shortLinkDAO.getDomain())
                    .shortUri(shortLinkDAO.getShortUri())
                    .favicon(Objects.equals(reqDTO.getOriginUrl(), shortLinkDAO.getOriginUrl()) ? shortLinkDAO.getFavicon() : getFavicon(reqDTO.getOriginUrl()))
                    .createdType(shortLinkDAO.getCreatedType())
                    .gid(reqDTO.getGid())
                    .originUrl(reqDTO.getOriginUrl())
                    .describe(reqDTO.getDescribe())
                    .validDateType(reqDTO.getValidDateType())
                    .validDate(reqDTO.getValidDate())
                    .build();
            baseMapper.update(shortLinkDAO, updateWrapper);
        } else {
            RReadWriteLock rwLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, reqDTO.getFullShortUrl()));
            RLock lock = rwLock.writeLock();
            lock.lock();
            try {
                LambdaUpdateWrapper<ShortLinkDAO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDAO.class)
                        .eq(ShortLinkDAO::getGid, shortLinkDAO.getGid())
                        .eq(ShortLinkDAO::getFullShortUrl, reqDTO.getFullShortUrl())
                        .eq(ShortLinkDAO::getDelFlag, 0)
                        .eq(ShortLinkDAO::getEnableStatus, 0)
                        .eq(ShortLinkDAO::getDelTime, 0L);
                ShortLinkDAO delShortLinkDAO = ShortLinkDAO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDAO.setDelFlag(1);
                baseMapper.update(delShortLinkDAO, updateWrapper);
                ShortLinkDAO newShortLinkDAO = ShortLinkDAO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(reqDTO.getOriginUrl())
                        .gid(reqDTO.getGid())
                        .createdType(shortLinkDAO.getCreatedType())
                        .validDateType(reqDTO.getValidDateType())
                        .validDate(reqDTO.getValidDate())
                        .describe(reqDTO.getDescribe())
                        .shortUri(shortLinkDAO.getShortUri())
                        .enableStatus(shortLinkDAO.getEnableStatus())
                        .totalUv(shortLinkDAO.getTotalUv())
                        .totalUip(shortLinkDAO.getTotalUip())
                        .totalPv(shortLinkDAO.getTotalPv())
                        .fullShortUrl(shortLinkDAO.getFullShortUrl())
                        .favicon(Objects.equals(reqDTO.getOriginUrl(), shortLinkDAO.getOriginUrl()) ? shortLinkDAO.getFavicon() : getFavicon(reqDTO.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(newShortLinkDAO); // 先删除旧，在复制原始数据新增一条
                LambdaQueryWrapper<ShortLinkGotoDAO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDAO.class)
                        .eq(ShortLinkGotoDAO::getGid, shortLinkDAO.getGid())
                        .eq(ShortLinkGotoDAO::getFullShortUrl, reqDTO.getFullShortUrl());
                ShortLinkGotoDAO shortLinkGotoDAO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.delete(linkGotoQueryWrapper);//删除
                shortLinkGotoDAO.setGid(reqDTO.getGid());// 修改gid
                shortLinkGotoMapper.insert(shortLinkGotoDAO);//新增
            } finally {
                lock.unlock();
            }
        }
        // 修改redis中的缓存
        if (!Objects.equals(shortLinkDAO.getValidDateType(), reqDTO.getValidDateType())
                || !Objects.equals(shortLinkDAO.getValidDate(), reqDTO.getValidDate())
                || !Objects.equals(shortLinkDAO.getOriginUrl(), reqDTO.getOriginUrl())) {
            //删除后当再次跳转时 restoreUrl 方法会根据以上三个条件再次设置redis，故只要删除就可以无需再设置
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, reqDTO.getFullShortUrl()));
        }
        //以前的 redis 已经过期
        if (shortLinkDAO.getValidDate() != null && shortLinkDAO.getValidDate().before(new Date())) {
            // 新的有效期是永久有效 或者 在未来某个时间点
            if (Objects.equals(reqDTO.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()) || reqDTO.getValidDate().after(new Date())) {
                //新的redis同样当再次跳转url时 restoreUrl 方法会设置
                stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, reqDTO.getFullShortUrl()));
            }
        }
    }
}
